"""
程序目标是下载网易云音乐中的歌曲。
支持三种模式：
1. 歌单模式：从歌单页面下载全部歌曲
2. 歌手模式：从歌手页面下载全部歌曲
3. 选择模式：从歌单或歌手页面选择特定歌曲下载

注意：付费音乐（VIP歌曲）可能无法下载
"""
import os
import time
import requests
import threading
import json
from lxml import etree
import tkinter as tk
from tkinter import ttk, messagebox, scrolledtext

class NetEaseMusic:
    def __init__(self):
        self.headers = {
            'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36'
        }
        # 歌曲详情API
        self.song_detail_url = "https://music.163.com/api/song/detail/?id={}&ids=[{}]"

    def get_music_list(self, mode, id_value, callback=None):
        """获取歌曲列表"""
        try:
            # 构造URL
            if mode == "1":
                url = f'https://music.163.com/playlist?id={id_value}'
                if callback: callback(f"正在获取歌单信息：{url}")
            else:
                url = f'https://music.163.com/artist?id={id_value}'
                if callback: callback(f"正在获取歌手信息：{url}")

            # 发起网络请求，获取网页中的数据
            response = requests.get(url, headers=self.headers)
            response.raise_for_status()

            # 将网页的html数据转换为树形结构
            html = etree.HTML(response.text)

            # 根据不同模式使用不同的XPath表达式筛选歌曲
            if mode == "2":  # 歌手模式
                music_label_list = html.xpath('//ul[@class="f-hide"]/li/a[contains(@href,"/song?")]')
            else:  # 歌单模式
                music_label_list = html.xpath('//a[contains(@href,"/song?")]')

            # 提取歌曲信息
            music_list = []
            song_ids = []
            
            for music_label in music_label_list:
                href = music_label.xpath('./@href')[0]
                music_id = href.split('=')[1]
                if music_id.isdigit():
                    music_name = music_label.xpath('./text()')[0]
                    music_list.append({
                        'id': music_id,
                        'name': music_name,
                        'fee': 0,  # 默认为免费歌曲
                        'status': "未知"
                    })
                    song_ids.append(music_id)

            # 批量获取歌曲详情，检查是否为付费歌曲
            if song_ids:
                if callback: callback("正在检查歌曲版权信息...")
                self.check_song_status(music_list, callback)

            return music_list

        except Exception as e:
            error_msg = f"获取歌曲列表失败：{str(e)}"
            if callback: callback(error_msg)
            raise Exception(error_msg)

    def check_song_status(self, music_list, callback=None):
        """检查歌曲的付费状态"""
        try:
            # 由于API限制，我们需要分批次请求歌曲详情
            batch_size = 100
            for i in range(0, len(music_list), batch_size):
                batch = music_list[i:i+batch_size]
                ids = [song['id'] for song in batch]
                ids_str = ','.join(ids)
                
                # 构造API请求
                url = f"https://music.163.com/api/song/detail/?ids=[{ids_str}]"
                response = requests.get(url, headers=self.headers)
                
                if response.status_code == 200:
                    data = response.json()
                    if 'songs' in data:
                        for song_detail in data['songs']:
                            song_id = str(song_detail['id'])
                            # 更新歌曲信息
                            for song in batch:
                                if song['id'] == song_id:
                                    # fee: 0-免费, 1-VIP可听, 4-购买专辑, 8-独家
                                    fee = song_detail.get('fee', 0)
                                    song['fee'] = fee
                                    
                                    if fee == 0:
                                        song['status'] = "免费"
                                    elif fee == 1:
                                        song['status'] = "VIP"
                                    elif fee == 4:
                                        song['status'] = "专辑"
                                    elif fee == 8:
                                        song['status'] = "独家"
                                    else:
                                        song['status'] = "未知"
                                    break
        except Exception as e:
            if callback: callback(f"检查歌曲状态时出错：{str(e)}")

    def download_single_music(self, music_info, callback=None):
        """下载单首歌曲"""
        try:
            music_id = music_info['id']
            music_name = music_info['name']
            music_status = music_info.get('status', '未知')

            if callback: callback(f"正在下载：《{music_name}》 [状态: {music_status}]")

            # 定义请求歌曲的链接
            music_url = 'http://music.163.com/song/media/outer/url?id=' + music_id

            # 发送网络请求获取歌曲数据
            response = requests.get(music_url, headers=self.headers)
            response.raise_for_status()

            # 检查是否获取到了有效的音乐文件
            if len(response.content) < 10000:
                if callback: callback(f"警告：《{music_name}》无法下载（可能是VIP限制或其他原因）")
                return False

            # 创建下载目录
            if not os.path.exists('music'):
                os.mkdir('music')

            # 将歌曲数据保存到mp3文件中
            safe_name = "".join(c for c in music_name if c.isalnum() or c in (' ', '-', '_', '(', ')', '[', ']'))
            file_path = f'./music/{safe_name}.mp3'
            
            with open(file_path, 'wb') as file:
                file.write(response.content)

            if callback: callback(f"《{music_name}》下载成功！")
            return True

        except Exception as e:
            if callback: callback(f"下载《{music_name}》时出错：{str(e)}")
            return False

    def download_music(self, mode, id_value, selected_songs=None, max_download=None, callback=None, update_progress=None):
        """下载音乐的主要逻辑"""
        try:
            # 获取歌曲列表
            music_list = self.get_music_list(mode, id_value, callback)

            if not music_list:
                if callback: callback("未找到任何歌曲，请检查页面ID是否正确")
                return music_list

            if callback: callback(f"\n找到 {len(music_list)} 首歌曲")
            if callback: callback("-" * 30)

            # 如果指定了要下载的歌曲，则只下载选中的歌曲
            if selected_songs is not None:
                music_list = [music for music in music_list if music['name'] in selected_songs]

            # 处理最大下载数量
            if max_download:
                music_list = music_list[:max_download]

            # 下载歌曲
            success_count = 0
            total_songs = len(music_list)

            for index, music_info in enumerate(music_list, 1):
                if self.download_single_music(music_info, callback):
                    success_count += 1

                # 更新进度条
                if update_progress:
                    progress = (index / total_songs) * 100
                    update_progress(progress)

                # 下载一首歌后延时1秒
                time.sleep(1)

            if callback: callback("\n" + "=" * 50)
            if callback: callback(f"下载完成！成功下载 {success_count}/{total_songs} 首歌曲")
            if callback: callback(f"音乐文件保存在 {os.path.abspath('./music')} 目录下")
            if callback: callback("=" * 50)

            return music_list

        except Exception as e:
            if callback: callback(f"程序出错：{str(e)}")
            return []

class NetEaseMusicGUI:
    def __init__(self, root):
        self.root = root
        self.root.title("网易云音乐下载器")
        self.root.geometry("900x600")
        self.root.resizable(True, True)
        
        self.netease = NetEaseMusic()
        self.music_list = []
        
        # 设置样式
        self.style = ttk.Style()
        self.style.configure("TButton", padding=6, relief="flat", background="#1DB954")
        self.style.configure("TLabel", font=("微软雅黑", 10))
        self.style.configure("TEntry", padding=6)
        
        # 创建主界面
        self.create_main_frame()
        
    def create_main_frame(self):
        """创建主界面"""
        self.main_frame = ttk.Frame(self.root, padding="20")
        self.main_frame.pack(fill=tk.BOTH, expand=True)
        
        # 标题
        ttk.Label(self.main_frame, text="网易云音乐下载器", font=("微软雅黑", 16, "bold")).pack(pady=10)
        
        # 左侧面板 - 控制区域
        left_panel = ttk.Frame(self.main_frame)
        left_panel.pack(side=tk.LEFT, fill=tk.Y, padx=(0, 10))
        
        # 模式选择
        mode_frame = ttk.LabelFrame(left_panel, text="下载模式", padding=10)
        mode_frame.pack(fill=tk.X, pady=(0, 10))
        self.mode_var = tk.StringVar(value="1")
        ttk.Radiobutton(mode_frame, text="歌单模式", variable=self.mode_var, value="1").pack(anchor=tk.W)
        ttk.Radiobutton(mode_frame, text="歌手模式", variable=self.mode_var, value="2").pack(anchor=tk.W)
        
        # ID输入
        id_frame = ttk.LabelFrame(left_panel, text="ID输入", padding=10)
        id_frame.pack(fill=tk.X, pady=(0, 10))
        self.id_label_var = tk.StringVar(value="歌单ID：")
        ttk.Label(id_frame, textvariable=self.id_label_var).pack(anchor=tk.W)
        self.id_var = tk.StringVar()
        ttk.Entry(id_frame, textvariable=self.id_var, width=30).pack(fill=tk.X, pady=(5, 0))
        
        # 监听模式变化，更新ID标签
        self.mode_var.trace("w", self.update_id_label)
        
        # 获取歌曲列表按钮
        ttk.Button(left_panel, text="获取歌曲列表", command=self.fetch_music_list).pack(fill=tk.X, pady=(0, 10))
        
        # 最大下载数量
        max_frame = ttk.LabelFrame(left_panel, text="下载设置", padding=10)
        max_frame.pack(fill=tk.X, pady=(0, 10))
        ttk.Label(max_frame, text="最大下载数量：").pack(anchor=tk.W)
        self.max_var = tk.StringVar()
        ttk.Entry(max_frame, textvariable=self.max_var, width=10).pack(fill=tk.X, pady=(5, 0))
        ttk.Label(max_frame, text="（留空下载全部）").pack()
        
        # 下载按钮
        ttk.Button(left_panel, text="开始下载", command=self.start_download).pack(fill=tk.X, pady=(0, 10))
        
        # 右侧面板 - 歌曲列表和日志
        right_panel = ttk.Frame(self.main_frame)
        right_panel.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
        
        # 歌曲列表
        list_frame = ttk.LabelFrame(right_panel, text="歌曲列表", padding=10)
        list_frame.pack(fill=tk.BOTH, expand=True, pady=(0, 10))
        
        # 创建一个框架来包含树形视图和滚动条
        tree_container = ttk.Frame(list_frame)
        tree_container.pack(fill=tk.BOTH, expand=True)
        
        # 创建垂直滚动条
        v_scrollbar = ttk.Scrollbar(tree_container)
        v_scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
        
        # 创建水平滚动条
        h_scrollbar = ttk.Scrollbar(tree_container, orient=tk.HORIZONTAL)
        h_scrollbar.pack(side=tk.BOTTOM, fill=tk.X)
        
        # 创建树形视图
        self.song_tree = ttk.Treeview(tree_container, 
                                     yscrollcommand=v_scrollbar.set,
                                     xscrollcommand=h_scrollbar.set,
                                     selectmode='extended')
        self.song_tree.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
        
        # 配置滚动条
        v_scrollbar.config(command=self.song_tree.yview)
        h_scrollbar.config(command=self.song_tree.xview)
        
        # 定义列
        self.song_tree['columns'] = ('id', 'name', 'status')
        
        # 格式化列
        self.song_tree.column('#0', width=0, stretch=tk.NO)  # 隐藏第一列
        self.song_tree.column('id', anchor=tk.CENTER, width=80)
        self.song_tree.column('name', anchor=tk.W, width=300)
        self.song_tree.column('status', anchor=tk.CENTER, width=80)
        
        # 创建表头
        self.song_tree.heading('#0', text='', anchor=tk.CENTER)
        self.song_tree.heading('id', text='ID', anchor=tk.CENTER)
        self.song_tree.heading('name', text='歌曲名称', anchor=tk.CENTER)
        self.song_tree.heading('status', text='状态', anchor=tk.CENTER)
        
        # 进度条
        self.progress_var = tk.DoubleVar()
        ttk.Progressbar(right_panel, variable=self.progress_var, maximum=100).pack(fill=tk.X, pady=(0, 10))
        
        # 日志区域
        log_frame = ttk.LabelFrame(right_panel, text="下载日志", padding=10)
        log_frame.pack(fill=tk.BOTH, expand=True)
        
        self.log_text = scrolledtext.ScrolledText(log_frame, wrap=tk.WORD, height=8)
        self.log_text.pack(fill=tk.BOTH, expand=True)
        self.log_text.config(state=tk.DISABLED)
        
        # 添加标签说明
        ttk.Label(right_panel, text="状态说明: 免费-可下载, VIP-需要会员, 专辑-需购买专辑, 独家-独家版权", 
                 font=("微软雅黑", 9)).pack(anchor=tk.W, pady=(5, 0))
        
    def update_id_label(self, *args):
        """根据选择的模式更新ID标签"""
        if self.mode_var.get() == "1":
            self.id_label_var.set("歌单ID：")
        else:
            self.id_label_var.set("歌手ID：")
            
    def fetch_music_list(self):
        """获取歌曲列表"""
        mode = self.mode_var.get()
        id_value = self.id_var.get()
        
        if not id_value:
            messagebox.showerror("错误", "请输入ID")
            return
            
        try:
            # 清空树形视图
            for item in self.song_tree.get_children():
                self.song_tree.delete(item)
            
            # 清空日志
            self.log_text.config(state=tk.NORMAL)
            self.log_text.delete(1.0, tk.END)
            self.log_text.config(state=tk.DISABLED)
            
            # 重置进度条
            self.progress_var.set(0)
            
            self.log("正在获取歌曲列表，请稍候...")
            
            # 在新线程中获取歌曲列表
            def fetch_thread():
                try:
                    # 获取歌曲列表
                    self.music_list = self.netease.get_music_list(mode, id_value, callback=self.log)
                    
                    # 在主线程中更新UI
                    self.root.after(0, self.update_song_tree)
                except Exception as e:
                    self.root.after(0, lambda: messagebox.showerror("错误", str(e)))
            
            threading.Thread(target=fetch_thread).start()
            
        except Exception as e:
            messagebox.showerror("错误", str(e))
    
    def update_song_tree(self):
        """更新歌曲列表树形视图"""
        # 清空树形视图
        for item in self.song_tree.get_children():
            self.song_tree.delete(item)
            
        # 将歌曲添加到树形视图
        for i, music in enumerate(self.music_list):
            # 根据状态设置标签颜色
            status = music.get('status', '未知')
            if status == "免费":
                tag = "free"
            elif status == "VIP":
                tag = "vip"
            elif status == "专辑":
                tag = "album"
            elif status == "独家":
                tag = "exclusive"
            else:
                tag = "unknown"
                
            self.song_tree.insert(parent='', index='end', iid=i, text='',
                               values=(music['id'], music['name'], status),
                               tags=(tag,))
        
        # 设置标签颜色
        self.song_tree.tag_configure('free', background='#E8F5E9')  # 浅绿色
        self.song_tree.tag_configure('vip', background='#FFECB3')   # 浅黄色
        self.song_tree.tag_configure('album', background='#E1F5FE') # 浅蓝色
        self.song_tree.tag_configure('exclusive', background='#F8BBD0') # 浅粉色
        self.song_tree.tag_configure('unknown', background='#EEEEEE') # 浅灰色
        
        self.log(f"成功获取到 {len(self.music_list)} 首歌曲")
            
    def log(self, message):
        """向日志区域添加消息"""
        self.log_text.config(state=tk.NORMAL)
        self.log_text.insert(tk.END, message + "\n")
        self.log_text.see(tk.END)
        self.log_text.config(state=tk.DISABLED)
        self.root.update()
        
    def update_progress(self, value):
        """更新进度条"""
        self.progress_var.set(value)
        self.root.update()
        
    def start_download(self):
        """开始下载"""
        if not self.music_list:
            messagebox.showerror("错误", "请先获取歌曲列表")
            return
            
        # 获取选中的歌曲
        selected_items = self.song_tree.selection()
        if not selected_items:
            response = messagebox.askyesno("确认", "未选择任何歌曲，是否下载全部歌曲？")
            if not response:
                return
            selected_songs = None
        else:
            selected_songs = []
            for item in selected_items:
                values = self.song_tree.item(item, 'values')
                selected_songs.append(values[1])  # 歌曲名称
            
        # 获取最大下载数量
        max_download_str = self.max_var.get()
        max_download = None
        if max_download_str.strip() and max_download_str.isdigit():
            max_download = int(max_download_str)
            
        # 清空日志
        self.log_text.config(state=tk.NORMAL)
        self.log_text.delete(1.0, tk.END)
        self.log_text.config(state=tk.DISABLED)
        
        # 重置进度条
        self.progress_var.set(0)
        
        # 在新线程中执行下载，避免界面卡顿
        def download_thread():
            self.netease.download_music(
                self.mode_var.get(),
                self.id_var.get(),
                selected_songs,
                max_download,
                callback=self.log,
                update_progress=self.update_progress
            )
            
        threading.Thread(target=download_thread).start()

def main():
    root = tk.Tk()
    app = NetEaseMusicGUI(root)
    root.mainloop()

if __name__ == "__main__":
    main()
